// Copyright 2012 Google Inc. All Rights Reserved.

package com.google.appengine.preferencessync.internal;

import com.google.appengine.preferencessync.adapter.AbstractSyncablePrefsService.RequestProcessor;
import com.google.appengine.preferencessync.internal.InternalSyncException.ExceptionType;
import com.google.appengine.preferencessync.internal.merge.ClientWinsHandler;
import com.google.appengine.preferencessync.internal.merge.CustomWins;
import com.google.appengine.preferencessync.internal.merge.InternalMergeHandler;
import com.google.appengine.preferencessync.internal.merge.ServerWinsHandler;
import com.google.appengine.preferencessync.internal.network.ClientIdUtils;
import com.google.appengine.preferencessync.internal.network.UpdatesFromServerCacher;
import com.google.appengine.preferencessync.internal.network.UpdatesToServerCacher;
import com.google.appengine.syncserver.shared.PropertyChange;

import android.content.Context;
import android.content.SharedPreferences;

import java.util.ArrayList;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Utility class for sync.
 */
public class InternalSyncUtils {

  // We maintain the old server updates in the UpdatesFromServerCacher.
  private static UpdatesFromServerCacher updatesFromServerCacher = null;

  // We maintain the updates to send in Updates to server Cacher.
  private static UpdatesToServerCacher updatesToServerCacher = null;

  /**
   * Calls sync for all preferences which have been specified to sync.
   *
   * @param config
   * @param context
   * @param requestProcessor
   * @throws InternalSyncException
   */
  public static synchronized void doSync(
      Config config, Context context, RequestProcessor requestProcessor)
      throws InternalSyncException {

    ClientIdUtils.mayBeSetClientId(config, requestProcessor);

    // If the list of preferences to sync in empty then don't do anything.
    if (config.getAllPreferencesToSync().isEmpty()) {
      return;
    }

    // Send any old updates remaining.
    if (updatesToServerCacher != null) {
      sendPageAndWriteServerCopyToDisk(context);
    } else if (updatesFromServerCacher != null) {
      // If the old cacher has stopped due to an exception then resume the old
      // caching, call merge and send updates.
      cacheAndMergeAndSendUpdates(context, config, requestProcessor);
    }

    // Do caching for rest of the preference files.
    for (String preference : config.getAllPreferencesToSync()) {
      // get updates from server cacher for the preferences.
      updatesFromServerCacher = getCacherFor(config, context, preference, requestProcessor);
      try {
        cacheAndMergeAndSendUpdates(context, config, requestProcessor);
      } catch (InternalSyncException e) {
        if (e.getType() == ExceptionType.INTERNAL_HTTP_STATUS_CONFLICT) {
          // In case there is a conflict we have start sync all over again.
          updatesFromServerCacher = null;
          updatesToServerCacher = null;
        }
        throw e;
      }
    }
  }

  /**
   * Get updates from the server and merge send updates from the server.
   *
   * @param context
   * @param config
   * @param requestProcessor
   * @throws InternalSyncException
   */
  private static void cacheAndMergeAndSendUpdates(
      Context context, Config config, RequestProcessor requestProcessor)
      throws InternalSyncException {
    // Start caching updates from server.
    long serverHeadVersion  = updatesFromServerCacher.resumeCaching();
    String sharedPrefsName = updatesFromServerCacher.getPreferencesName();

    SyncClientLog.logDebug(InternalSyncUtils.class, "Syncing for preference " + sharedPrefsName);

    InternalMergeHandler mergeHandler = null;
    // Check what's the win scenario and then do the sync.
    switch (config.getResolutionType(sharedPrefsName)) {
      case CLIENT_WINS:
        mergeHandler = new ClientWinsHandler();
        break;

      case SERVER_WINS:
        mergeHandler = new ServerWinsHandler();
        break;

      case CUSTOM_WINS:
        mergeHandler = new CustomWins(context, sharedPrefsName);
        break;
    }

    SharedPreferences sharedPrefs = context.getSharedPreferences(sharedPrefsName,
        Context.MODE_PRIVATE);
    ArrayList<PropertyChange> clientChanges = getClientChanges(context, sharedPrefsName);
    ArrayList<PropertyChange> serverChanges = updatesFromServerCacher.getPropertyChanges();

    SyncClientLog.logDebug(InternalSyncUtils.class, "Server changes recieved:");
    logChanges(serverChanges);

    // Call merge.
    SyncClientLog.logDebug(InternalSyncUtils.class, "Calling merge handler" + mergeHandler
        .getClass().getCanonicalName());
    mergeHandler.merge(sharedPrefs, clientChanges, serverChanges);

    // Compute changes to send to the server.
    ArrayList<PropertyChange> changesToSend = getChangesToSend(context, sharedPrefsName,
        serverChanges);

    SyncClientLog.logDebug(InternalSyncUtils.class, "Client changes to be sent:");
    logChanges(changesToSend);

    updatesToServerCacher = new UpdatesToServerCacher(config, changesToSend, sharedPrefsName,
        serverHeadVersion, requestProcessor);

    // We have already computed the updates to server we don't need updates from server anymore.
    updatesFromServerCacher = null;

    // Before sending make sure the contents of shared preferences are written to disk.
    context.getSharedPreferences(sharedPrefsName, Context.MODE_PRIVATE).edit().commit();

    sendPageAndWriteServerCopyToDisk(context);
  }

  /**
   * Resume sending pages.
   *
   * @param context
   * @throws InternalSyncException
   */
  private static void sendPageAndWriteServerCopyToDisk(Context context)
      throws InternalSyncException {

    // Server head version.
    long newServerHeadVersion = updatesToServerCacher.resumeSendingPages();

    String preferenceName = updatesToServerCacher.getPreferencesName();
    SharedPreferences sharedPreference = context.getSharedPreferences(preferenceName,
        Context.MODE_PRIVATE);

    // Store the server copy shared preference locally.
    // (The invariant after sync is "server copy = client copy")
    getServerSharedPreferences(context, preferenceName).overwriteServerPrefereneces(
        newServerHeadVersion, sharedPreference);
    updatesToServerCacher = null;
  }

  /**
   * Allocates cacher for the preferences.
   *
   * @param config
   * @param context
   * @param preferences
   * @return Cacher which can receive updates from the server.
   * @throws InternalSyncException
   */
  private static UpdatesFromServerCacher getCacherFor(Config config, Context context,
      String preferences, RequestProcessor requestProcessor) throws InternalSyncException {
    long lastSyncHeadVersion = getServerSharedPreferences(context, preferences).getMaxVersion();
    return new UpdatesFromServerCacher(config, preferences, lastSyncHeadVersion, requestProcessor);
  }

  /**
   * @param context
   * @param preferences
   * @return SharedPreference at the server end stored locally.
   */
  private static ServerSharedPreferences getServerSharedPreferences(Context context,
      String preferences) {
    SharedPreferences sharedPreferences = context.getSharedPreferences(
        getSharedPreferencesName(preferences), Context.MODE_PRIVATE);
    return new ServerSharedPreferences(sharedPreferences);
  }

  /**
   * Clear all preference metadata.
   */
  public static void clearMetadata(Context context) throws InternalSyncException {
    Config config = Config.getDefaultConfig(context);
    ArrayList<String> prefs = config.getAllPreferencesToSync();
    for (String pref : prefs) {
      getServerSharedPreferences(context, pref).clear();
    }
    config.setDeviceIdSent(false);
  }

  private static String getSharedPreferencesName(String preferences) {
    return Config.SERVER_COPY_PREFIX + preferences;
  }

  /**
   * Compute client changes since the last sync.
   */
  private static ArrayList<PropertyChange> getClientChanges(Context context, String preferences)
      throws InternalSyncException {
    // The locally stored server map during after after last successful sync
    // = client map after same sync.
    Map<String, String> lastSyncServerMap = getServerSharedPreferences(context, preferences).
        getAll();
    SharedPreferences sharedPrefs = context.getSharedPreferences(preferences, Context.MODE_PRIVATE);
    return SharedPreferencesUtils.diffSharedPreferences(lastSyncServerMap, sharedPrefs);
  }

  /**
   * @param context
   * @param preferences
   * @param serverChanges
   * @return List of changes to send to server depending changes made in the server, server state
   *    during last sync (stored locally) and current client state.
   * @throws InternalSyncException
   */
  private static ArrayList<PropertyChange> getChangesToSend(Context context, String preferences,
      ArrayList<PropertyChange> serverChanges)
      throws InternalSyncException {
    logSharedPrefs(context, getSharedPreferencesName(preferences));
    Map<String, String> currentServerMap = getServerSharedPreferences(context, preferences)
        .getMapWithChangesApplied(serverChanges);

    logMap(currentServerMap);
    logSharedPrefs(context, preferences);
    SharedPreferences sharedPrefs = context.getSharedPreferences(preferences, Context.MODE_PRIVATE);
    return SharedPreferencesUtils.diffSharedPreferences(currentServerMap, sharedPrefs);
  }

  /**
   * Print change list.
   */
  private static void logChanges(ArrayList<PropertyChange> changeList) {
    for (PropertyChange change : changeList) {
      SyncClientLog.logDebug(InternalSyncUtils.class, change.getPrettyString());
    }
  }

  private static void logSharedPrefs(Context context, String preferencesName)
      throws InternalSyncException {
    SharedPreferences sharedPrefs =
        context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE);
    SyncClientLog.logDebug(InternalSyncUtils.class,
        String.format("Print preferences = %s.", preferencesName));
    for (Map.Entry<String, ?> entry : sharedPrefs.getAll().entrySet()) {
      SyncClientLog.logDebug(InternalSyncUtils.class,
          String.format("Key %s Value %s", entry.getKey(),
              SharedPreferencesUtils.getStringValue(entry.getValue())));
    }
  }

  private static void logMap(Map<String, String> map) {
    for (Entry<String, String> entry : map.entrySet()) {
      SyncClientLog.logDebug(InternalSyncUtils.class,
          String.format("map:key = %s,value = %s", entry.getKey(), entry.getValue()));
    }
  }
}
